#pragma once
#include <vector>
#include <map>
#include <unordered_map>
#include <set>
#include <string>

#include <iostream>
/*
#include <iostream>
#include <tuple>
#include <type_traits>
#include <map>

#include "BinPack.hpp"
#include <array>

struct MyStructsub
{
	uint8_t x;
	char z;
	std::string str;
	int i1;

	template<class T>
	void pack(T& avValue) {
		avValue(x, z, str, i1);
	}
};

struct MyStruct {
	int64_t x;
	int y;
	std::array<char, 10> z;
	BinPack::Wrapper<std::string> str;
	BinPack::Wrapper<std::vector<uint8_t>> va;

	std::map<int32_t, int32_t> testMap;

	MyStructsub sub;

	int xx;

	template<class T>
	void pack(T& avValue) {
		avValue(x, z, str, va, xx, testMap, y, sub);
	}
};

struct baseTest
{
	int a;

	template<class T>
	void pack(T& avValue) {
		avValue(a);
	}
};

struct subTest : public baseTest
{
	int b;
	template<class T>
	void pack(T& avValue) {
		baseTest::pack(avValue);
		avValue(b);
	}
};

int main() {
	subTest sub = { 0 };
	sub.a = 10;
	sub.b = 20;
	auto data1 = BinPack::pack(sub);

	subTest subB = BinPack::unpack<subTest>(data1);



	MyStruct s = {0};
	s.x = 10165465498789;
	s.y = 1234321;
	s.z[0] = 'z';
	s.z[1] = 'z';
	s.z[8] = 'z';
	s.str = "abvfr";
	s.testMap[1] = 100;
	s.testMap[2] = 200;
	s.sub.i1 = 123456;
	s.sub.x = 100;
	s.sub.z = 30;
	s.va.value.push_back(1);
	s.va.value.push_back(2);
	s.va.value.push_back(3);
	s.sub.str = "afdasf";
	s.xx = 654321;
	auto data = BinPack::pack(s);
	MyStruct A = BinPack::unpack<MyStruct>(data);

}

*/

//support up C++17

namespace BinPack {
	template<class T>
	struct is_container {
		static const bool value = false;
	};

	template<class T, class Alloc>
	struct is_container<std::vector<T, Alloc> > {
		static const bool value = true;
	};

	template<class T, class Alloc>
	struct is_container<std::list<T, Alloc> > {
		static const bool value = true;
	};

	template<class T, class Alloc>
	struct is_container<std::map<T, Alloc> > {
		static const bool value = true;
	};

	template<class T, class Alloc>
	struct is_container<std::unordered_map<T, Alloc> > {
		static const bool value = true;
	};

	template<class T, class Alloc>
	struct is_container<std::set<T, Alloc> > {
		static const bool value = true;
	};

	template<class T>
	struct is_stdarray {
		static const bool value = false;
	};

	template<class T, std::size_t N>
	struct is_stdarray<std::array<T, N>> {
		static const bool value = true;
	};

	template<class T>
	struct is_map {
		static const bool value = false;
	};

	template<class T, class Alloc>
	struct is_map<std::map<T, Alloc> > {
		static const bool value = true;
	};

	template<class T, class Alloc>
	struct is_map<std::unordered_map<T, Alloc> > {
		static const bool value = true;
	};

	class Packer {
	public:
		template<class ... Types>
		size_t operator()(const Types &... args) {
			size_t totalSize = 0;
			((totalSize += pack_type(std::forward<const Types&>(args))), ...);
			return totalSize;
		}
		const std::vector<uint8_t>& vector() const {
			return serialized_object;
		}

		void clear() {
			serialized_object.clear();
		}
	private:
		std::vector<uint8_t> serialized_object;
		bool LittleEndian = true;

		template <typename T>
		inline size_t pakcArithmetic(const T& value) {
			const uint8_t* bytePtr = reinterpret_cast<const uint8_t*>(&value);
			if (!LittleEndian) {
				for (int i = sizeof(T) - 1; i >= 0; --i) {
					serialized_object.push_back(bytePtr[i]);
				}
			}
			else {
				for (int i = 0; i < sizeof(T); ++i) {
					serialized_object.push_back(bytePtr[i]);
				}
			}
			return sizeof(T);
		}

		template<class T>
		size_t pack_type(const T& value) {
			if constexpr (is_map<T>::value) {
				return pack_map(value);
			}
			else if constexpr (is_container<T>::value || is_stdarray<T>::value) {
				return pack_array(value);
			}
			else if constexpr (std::is_integral_v<T>) {
				return pakcArithmetic(value);
			}
			else if constexpr (std::is_floating_point_v<T>) {
				return pakcArithmetic(value);
			}
			else {
				auto recursive_packer = Packer{};
				const_cast<T&>(value).pack(recursive_packer);

				serialized_object.insert(serialized_object.end(), recursive_packer.vector().begin(), recursive_packer.vector().end());
				return recursive_packer.vector().size();
			}
		}

		template<class T>
		size_t pack_array(const T& array) {
			uint32_t lvSize = (uint32_t)array.size();
			pack_type(lvSize);

			for (const auto& elem : array) {
				pack_type(elem);
			}
			return lvSize;
		}

		template<class T>
		size_t pack_map(const T& map) {
			uint32_t lvSize = (uint32_t)map.size();
			pack_type(lvSize);

			for (const auto& elem : map) {
				pack_type(std::get<0>(elem));
				pack_type(std::get<1>(elem));
			}
			return lvSize;
		}

		template<>
		size_t pack_type(const std::string& value) {
			uint32_t length = (uint32_t)value.size();
			pack_type(length);
			for (char i : value) {
				serialized_object.emplace_back(static_cast<uint8_t>(i));
			}
			return length;
		}
	};

	template<class PackableObject>
	std::vector<uint8_t> pack(PackableObject& obj, bool LittleEndian = true) {
		auto packer = Packer{};
		obj.pack(packer);
		return packer.vector();
	}

	template<class PackableObject>
	std::vector<uint8_t> pack(PackableObject&& obj, bool LittleEndian = true) {
		auto packer = Packer{};
		obj.pack(packer);
		return packer.vector();
	}

	class unPacker {
	public:
		unPacker() : data_pointer(nullptr), data_end(nullptr) {};

		unPacker(const uint8_t* data_start, std::size_t bytes)
			: data_pointer(data_start), data_end(data_start + bytes) {};

		template<class ... Types>
		size_t operator()(Types &... args) {
			return (unpack_type(std::forward<Types&>(args)), ...);
		}

	private:
		const uint8_t* data_pointer;
		const uint8_t* data_end;
		bool LittleEndian = true;
		int unPackSize = 0;

		template <typename T>
		size_t unpakcArithmetic(T& value) {
			if (!LittleEndian)
			{
				for (int i = 0; i < sizeof(T); ++i) {
					if constexpr (std::_Is_character_or_bool<T>::value)
					{
						value = T(*getData(1)) << 8 * (sizeof(T) - i - 1);
					}
					else
					{
						value += T(*getData(1)) << 8 * (sizeof(T) - i - 1);
					}
				}
			}
			else {
				for (int i = sizeof(T) - 1; i >= 0 ; --i) {
					if constexpr (std::_Is_character_or_bool<T>::value)
					{
						value = T(*getData(1)) << 8 * (sizeof(T) - i - 1);
					}
					else
					{
						value += T(*getData(1)) << 8 * (sizeof(T) - i - 1);
					}
				}
			}
			return sizeof(T);
		}

		template<class T>
		size_t unpack_type(T& value) {
			if constexpr (is_map<T>::value) {
				return unpack_map(value);
			}
			else if constexpr (is_container<T>::value) {
				return unpack_array(value);
			}
			else if constexpr (is_stdarray<T>::value) {
				return unpack_stdarray(value);
			}
			else if constexpr (std::is_integral_v<T>) {
				return unpakcArithmetic(value);
			}
			else if constexpr (std::is_floating_point_v<T>) {
				return unpakcArithmetic(value);
			}
			else {
				auto recursive_unpacker = unPacker(data_pointer, data_end - data_pointer);
				value.unPack(recursive_unpacker);
				getData(recursive_unpacker.unPackSize);
				return recursive_unpacker.unPackSize;
			}
		}

		template<>
		size_t unpack_type(std::string& value) {
			uint32_t length = 0;
			unpakcArithmetic(length);
			const uint8_t* strB = getData(length);
			value = std::string(strB, data_pointer);
			return length;
		}

		const uint8_t* getData(int count) {
			auto result = data_pointer;
			data_pointer += count;
			unPackSize += count;
			return result;
		}

		template<class T>
		size_t unpack_stdarray(T& array) {
			using ValueType = typename T::value_type;
			auto vec = std::vector<ValueType>{};
			size_t result = unpack_array(vec);
			std::copy(vec.begin(), vec.end(), array.begin());
			return result;
		}

		template<class T>
		size_t unpack_array(T& array) {
			using ValueType = typename T::value_type;
			uint32_t size = 0;
			unpakcArithmetic(size);

			array.reserve(size);
			for (uint32_t i = 0; i < size; ++i)
			{
				ValueType val{};
				unpack_type(val);
				array.push_back(std::move(val));
			}
			return size;
		}
		template<class T>
		size_t unpack_map(T& map) {
			using KeyType = typename T::key_type;
			using MappedType = typename T::mapped_type;

			uint32_t size = 0;
			unpakcArithmetic(size);

			for (int i = 0; i < size; ++i)
			{
				KeyType key{};
				MappedType value{};
				unpack_type(key);
				unpack_type(value);
				map.insert_or_assign(key, value);
			}
			return size;
		}
	};

	template<class UnpackableObject>
	UnpackableObject unpack(const uint8_t* data_start, const std::size_t size, bool LittleEndian = true) {
		auto obj = UnpackableObject{};
		auto unpacker = unPacker(data_start, size);
		obj.unPack(unpacker);
		return obj;
	}


	template<class UnpackableObject>
	UnpackableObject unpack(const std::vector<uint8_t>& data, bool LittleEndian = true) {
		return unpack<UnpackableObject>(data.data(), data.size());
	}

}